Micron Document
baubs git

Node / mirrors / respira / files / src / formats / import / worker.ts

Displaying Raw • Download

src/formats/import/worker.ts copilot/create-shared-infocard-component (7c3f79ae) Text, 12.93 KB

import { loadPyodide, type PyodideInterface } from "pyodide";
import { STITCH, MOVE, TRIM, END } from "./constants";
import { encodeStitchesToPen } from "../pen/encoder";

// Message types from main thread
export type WorkerMessage =
| { type: "INITIALIZE"; pyodideIndexURL?: string; pystitchWheelURL?: string }
| { type: "CONVERT_PES"; fileData: ArrayBuffer; fileName: string };

// Response types to main thread
export type WorkerResponse =
| { type: "INIT_PROGRESS"; progress: number; step: string }
| { type: "INIT_COMPLETE" }
| { type: "INIT_ERROR"; error: string }
| {
type: "CONVERT_COMPLETE";
data: {
stitches: number[][];
threads: Array<{
color: number;
hex: string;
brand: string | null;
catalogNumber: string | null;
description: string | null;
chart: string | null;
}>;
uniqueColors: Array<{
color: number;
hex: string;
brand: string | null;
catalogNumber: string | null;
description: string | null;
chart: string | null;
threadIndices: number[];
}>;
penData: number[]; // Serialized as array
colorCount: number;
stitchCount: number;
bounds: {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
};
}
| { type: "CONVERT_ERROR"; error: string };

console.log("[PatternConverterWorker] Worker script loaded");

let pyodide: PyodideInterface | null = null;
let isInitializing = false;

// JavaScript constants module to expose to Python
const jsEmbConstants = {
STITCH,
MOVE,
TRIM,
END,
};

/**
* Initialize Pyodide with progress tracking
*/
async function initializePyodide(
pyodideIndexURL?: string,
pystitchWheelURL?: string,
) {
if (pyodide) {
return; // Already initialized
}

if (isInitializing) {
throw new Error("Initialization already in progress");
}

isInitializing = true;

try {
self.postMessage({
type: "INIT_PROGRESS",
progress: 0,
step: "Starting initialization...",
} as WorkerResponse);

console.log("[PyodideWorker] Loading Pyodide runtime...");

self.postMessage({
type: "INIT_PROGRESS",
progress: 10,
step: "Loading Python runtime...",
} as WorkerResponse);

// Load Pyodide runtime
// Use provided URL or default to /assets/
const indexURL = pyodideIndexURL || "/assets/";
console.log("[PyodideWorker] Pyodide index URL:", indexURL);

pyodide = await loadPyodide({
indexURL: indexURL,
});

console.log("[PyodideWorker] Pyodide runtime loaded");

self.postMessage({
type: "INIT_PROGRESS",
progress: 70,
step: "Python runtime loaded",
} as WorkerResponse);

self.postMessage({
type: "INIT_PROGRESS",
progress: 75,
step: "Loading pystitch library...",
} as WorkerResponse);

// Load pystitch wheel
// Use provided URL or default
const wheelURL = pystitchWheelURL || "/pystitch-1.0.0-py3-none-any.whl";
console.log("[PyodideWorker] Pystitch wheel URL:", wheelURL);

await pyodide.loadPackage(wheelURL);

console.log("[PyodideWorker] pystitch library loaded");

self.postMessage({
type: "INIT_PROGRESS",
progress: 100,
step: "Ready!",
} as WorkerResponse);

self.postMessage({
type: "INIT_COMPLETE",
} as WorkerResponse);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : "Unknown error";
console.error("[PyodideWorker] Initialization error:", err);

self.postMessage({
type: "INIT_ERROR",
error: errorMsg,
} as WorkerResponse);

throw err;
} finally {
isInitializing = false;
}
}

/**
* Convert PES file to PEN format
*/
async function convertPesToPen(fileData: ArrayBuffer) {
if (!pyodide) {
throw new Error("Pyodide not initialized");
}

try {
// Register our JavaScript constants module for Python to import
pyodide.registerJsModule("js_emb_constants", jsEmbConstants);

// Convert to Uint8Array
const uint8Array = new Uint8Array(fileData);

// Write file to Pyodide virtual filesystem
const tempFileName = "/tmp/pattern.pes";
pyodide.FS.writeFile(tempFileName, uint8Array);

// Read the pattern using PyStitch (same logic as original converter)
const result = await pyodide.runPythonAsync(\\`
import pystitch
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
from js_emb_constants import STITCH as JS_STITCH, MOVE as JS_MOVE, TRIM as JS_TRIM, END as JS_END

# Read the PES file
pattern = pystitch.read('${tempFileName}')

def map_cmd(pystitch_cmd):
"""Map PyStitch command to our JavaScript constant values

This ensures we have known, consistent values regardless of PyStitch's internal values.
Our JS constants use pyembroidery-style bitmask values:
STITCH = 0x00, MOVE/JUMP = 0x10, TRIM = 0x20, END = 0x100
"""
if pystitch_cmd == STITCH:
return JS_STITCH
elif pystitch_cmd == JUMP:
return JS_MOVE # PyStitch JUMP maps to our MOVE constant
elif pystitch_cmd == TRIM:
return JS_TRIM
elif pystitch_cmd == END:
return JS_END
else:
# For any other commands, preserve as bitmask
result = JS_STITCH
if pystitch_cmd & JUMP:
result |= JS_MOVE
if pystitch_cmd & TRIM:
result |= JS_TRIM
if pystitch_cmd & END:
result |= JS_END
return result

# Use the raw stitches list which preserves command flags
# Each stitch in pattern.stitches is [x, y, cmd]
# We need to assign color indices based on COLOR_CHANGE commands
# and filter out COLOR_CHANGE and STOP commands (they're not actual stitches)
#
# IMPORTANT: In PES files, COLOR_CHANGE commands can appear before finishing
# stitches (tack/lock stitches) that semantically belong to the PREVIOUS color.
# We need to detect this pattern and assign colors correctly.

stitches_with_colors = []
current_color = 0

for i, stitch in enumerate(pattern.stitches):
x, y, cmd = stitch

# Check for color change command
if cmd == COLOR_CHANGE:
current_color += 1
continue

# Check for stop command - skip it
if cmd == STOP:
continue

# Check for standalone END command (no stitch data)
if cmd == END:
continue

# PyStitch inserts duplicate stitches at the same coordinates during color changes
# Skip any stitch that has the exact same position as the previous one
if len(stitches_with_colors) > 0:
last_stitch = stitches_with_colors[-1]
last_x, last_y = last_stitch[0], last_stitch[1]

if x == last_x and y == last_y:
# Duplicate position - skip it
continue

# Add actual stitch with current color index and mapped command
mapped_cmd = map_cmd(cmd)
stitches_with_colors.append([x, y, mapped_cmd, current_color])

# Convert to JSON-serializable format
{
'stitches': stitches_with_colors,
'threads': [
{
'color': thread.color if hasattr(thread, 'color') else 0,
'hex': thread.hex_color() if hasattr(thread, 'hex_color') else '#000000',
'catalog_number': thread.catalog_number if hasattr(thread, 'catalog_number') else -1,
'brand': thread.brand if hasattr(thread, 'brand') else "",
'description': thread.description if hasattr(thread, 'description') else "",
'chart': thread.chart if hasattr(thread, 'chart') else ""
}
for thread in pattern.threadlist
],
'thread_count': len(pattern.threadlist),
'stitch_count': len(stitches_with_colors),
'color_changes': current_color
}
\\`);

// Convert Python result to JavaScript
const data = result.toJs({ dict_converter: Object.fromEntries });

// Clean up virtual file
try {
pyodide.FS.unlink(tempFileName);
} catch {
// Ignore errors
}

// Extract stitches and validate
const stitches: number[][] = Array.from(
data.stitches as ArrayLike<ArrayLike<number>>,
).map((stitch) => Array.from(stitch));

if (!stitches || stitches.length === 0) {
throw new Error("Invalid PES file or no stitches found");
}

// Extract thread data - preserve null values for unavailable metadata
const threads = (
data.threads as Array<{
color?: number;
hex?: string;
catalog_number?: number | string;
brand?: string;
description?: string;
chart?: string;
}>
).map((thread) => {
// Normalize catalog_number - can be string or number from PyStitch
const catalogNum = thread.catalog_number;
const normalizedCatalog =
catalogNum !== undefined &&
catalogNum !== null &&
catalogNum !== -1 &&
catalogNum !== "-1" &&
catalogNum !== ""
? String(catalogNum)
: null;

return {
color: thread.color ?? 0,
hex: thread.hex || "#000000",
catalogNumber: normalizedCatalog,
brand: thread.brand && thread.brand !== "" ? thread.brand : null,
description:
thread.description && thread.description !== ""
? thread.description
: null,
chart: thread.chart && thread.chart !== "" ? thread.chart : null,
};
});

// Encode stitches to PEN format using the extracted encoder
console.log("[patternConverter] Encoding stitches to PEN format...");
console.log(" - Input stitches:", stitches);
const { penBytes: penStitches, bounds } = encodeStitchesToPen(stitches);
const { minX, maxX, minY, maxY } = bounds;

// Calculate unique colors from threads (threads represent color blocks, not unique colors)
const uniqueColors = threads.reduce(
(acc, thread, idx) => {
const existing = acc.find((c) => c.hex === thread.hex);
if (existing) {
existing.threadIndices.push(idx);
} else {
acc.push({
color: thread.color,
hex: thread.hex,
brand: thread.brand,
catalogNumber: thread.catalogNumber,
description: thread.description,
chart: thread.chart,
threadIndices: [idx],
});
}
return acc;
},
[] as Array<{
color: number;
hex: string;
brand: string | null;
catalogNumber: string | null;
description: string | null;
chart: string | null;
threadIndices: number[];
}>,
);

// Calculate PEN stitch count (should match what machine will count)
const penStitchCount = penStitches.length / 4;

console.log("[patternConverter] PEN encoding complete:");
console.log(\\` - PyStitch stitches: ${stitches.length}\\`);
console.log(\\` - PEN bytes: ${penStitches.length}\\`);
console.log(\\` - PEN stitches (bytes/4): ${penStitchCount}\\`);
console.log(\\` - Bounds: (${minX}, ${minY}) to (${maxX}, ${maxY})\\`);

// Post result back to main thread
self.postMessage({
type: "CONVERT_COMPLETE",
data: {
stitches,
threads,
uniqueColors,
penData: penStitches, // Send as array (will be converted to Uint8Array in main thread)
colorCount: data.thread_count,
stitchCount: data.stitch_count,
bounds: {
minX: minX === Infinity ? 0 : minX,
maxX: maxX === -Infinity ? 0 : maxX,
minY: minY === Infinity ? 0 : minY,
maxY: maxY === -Infinity ? 0 : maxY,
},
},
} as WorkerResponse);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : "Unknown error";
console.error("[PyodideWorker] Conversion error:", err);

self.postMessage({
type: "CONVERT_ERROR",
error: errorMsg,
} as WorkerResponse);

throw err;
}
}

// Handle messages from main thread
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
const message = event.data;
console.log("[PatternConverterWorker] Received message:", message.type);

try {
switch (message.type) {
case "INITIALIZE":
console.log("[PatternConverterWorker] Starting initialization...");
await initializePyodide(
message.pyodideIndexURL,
message.pystitchWheelURL,
);
break;

case "CONVERT_PES":
console.log("[PatternConverterWorker] Starting PES conversion...");
await convertPesToPen(message.fileData);
break;

default:
console.error(
"[PatternConverterWorker] Unknown message type:",
message,
);
}
} catch (err) {
console.error("[PatternConverterWorker] Error handling message:", err);
}
};

console.log("[PatternConverterWorker] Message handler registered");

Served by rngit 1.3.3 - Generated in 0.05s